.. _ref_auto_adm_ldap:

Автоматизация задач администрирования через LDAP-запросы
--------------------------------------------------------

Введение
~~~~~~~~

Автоматизация позволяет ускорить выполнение массовых операций администрирования, например, если требуется завести тысячу пользователей, следует написать скрипт для импорта учетных записей из **CSV-файла**, автоматически распределяющий пользователей по подразделениям и в соответствующие группы. Этот способ позволяет экономить время и исключает ошибки, связанные с человеческим фактором.

В данной инструкции описано устройство **LDAP-каталога**, алгоритм управления доменом с помощью прямых запросов к каталогу, а также несколько полезных запросов для решения реальных задач администрирования.

Технология LDAP
~~~~~~~~~~~~~~~~~~~~~~~~~

Служба каталога **ALD Pro** построена на базе 389 Directory Server, который реализует функции каталога и поддерживает протокол **LDAPv3**. Облегченный протокол доступа к данным каталога (Lightweight Directory Access Protocol, **LDAP**) является упрощенной модификацией более строгих стандартов для построения службы распределенного каталога сети X.500.

Каталог **LDAP** является специализированной нереляционной базой данных, файлы которой расположены в папке ``/var/lib/dirsrv/slapd-ald-company-lan/db``. Информация каталога представлена в виде древовидной структуры, которую также называют **Directory Information Tree** или сокращенно **DIT**. См. :ref:`ldap-requests 1`

.. figure:: media/рис1.png
   :name: ldap-requests 1

   Структура каталога Directory Information Tree

Корень каталога называется **Root DSE**, где **DSE** означает **Directory system agent Specific Entry**, то есть специализированная запись агента системы директорий. Эта запись не имеет родителя, и она описана в файле ``/etc/dirsrv/slapd-ALD-COMPANY-LAN/dse.ldif``, где ``slapd-ALD-COMPANY-LAN`` - директория сервиса ``slapd`` c именем домена.

Корень **Root DSE** является родителем для записей ``dc=ald,dc=company,dc=lan`` и ``cn=changelog``, которые так же называют базовыми записями, корневыми суффиксами или контекстами именования (base entry, root suffix, naming context). В контексте ``dc=ald,dc=company,dc=lan`` хранятся все объекты каталога, а в ``cn=changelog`` – журнал изменений для работы плагина ``Retro Changelog Plugin``. Все контексты, определенные в каталоге, указаны в операционном атрибуте ``namingContexts`` записи **Root DSE**, однако спецификация **LDAP** позволяет также использовать служебные контексты, например, в записи ``cn=config`` представлены настройки каталога, а через запись ``cn=monitor`` можно получить доступ к информации о состоянии сервера в режиме реального времени.

Существуют разные подходы в задании наименований корневых суффиксов, в **ALD Pro** (FreeIPA) используются правила спецификации RFC-2247, поэтому для домена ``ald.company.lan`` имя корневого суффикса будет ``dc=ald,dc=company,dc=lan``, где ``dc`` – компонент имени домена, сокращение от **Domain Component**. Правила наименования необходимы для возможности преобразования DNS-имени в имя **LDAP-записи** и наоборот.

От корневого суффикса ``dc=ald,dc=company,dc=lan`` ответвляются дочерние записи (Entry), которые, в свою очередь, могут быть контейнерами для других записей, за счет чего и образуется древовидная структура. Таким образом, записи каталога можно сравнить с директориями файловой системы.

У каждой записи каталога есть имя, которое должно быть уникальным в пределах родительского контейнера, поэтому оно называется относительно уникальным именем **Relative Distinguished Name** или кратко **RDN**. Учетные записи пользователей, например, имеют имена ``uid=admin``, ``uid=ivan.kuznetsov`` и т.д. Особенность имен объектов в **LDAP** заключается в том, что они хранят не конкретные значения, а только ссылки на хранимые атрибуты записей, которые используются для идентификации объектов. Например, для идентификации учетных записей пользователей используют атрибут ``uid``, для учетных записей компьютеров ``FQDN`` (fully qualified domain name, полное доменное имя хоста), а в именах контейнеров обычно присутствует ``cn`` (common name, общее имя).

В приведенном примере **RDN** учетной записи доменного администратора ``uid=admin`` состоит из названия атрибута ``uid``, после которого идет символ присвоения ``=`` и далее значение атрибута ``admin``. Вся эта запись вместе называется "Определением Значения Атрибута" - **Attribute Value Assertion (AVA)**. Обычно имена записей задаются значением одного атрибута, но могут использоваться и несколько, тогда в имени **RDN** эти определения AVA будут объединяться знаком ``+``, например: ``cn=Ivan+l=Moscow``. Каталог 389 Directory Server поддерживает такой способ именования записей, но в **ALD Pro** (FreeIPA) он не используется.

Для идентификации объекта в пределах всего каталога используют уникальное имя **Distinguished Name** или сокращенно **DN**. Уникальное имя представляет из себя цепочку **RDN**, которые записывают через запятую слева направо, начиная с целевой записи и до корневого суффикса вверх по иерархии, см. :ref:`ldap-requests 2`. Если привести аналогию с объектами файловой системы, то **RDN** будет соответствовать имени объекта директории или файла, а **DN** – полному имени объекта файловой системы, которое включает путь к родительскому объекту и имени объекта. И также как на одном диске не может быть двух директорий с одинаковыми полными именами, в каталоге **LDAP** не может быть двух записей с одинаковыми **DN**. Описание формата **DN** можно найти RFC 4514.

.. figure:: media/рис2.png
   :name: ldap-requests 2

   Пример формирования уникального имени записи DN

Запись каталога может хранить не только дочерние записи, но и набор атрибутов, описывающих ее свойства. Например, для учетной записи пользователя это могут быть ФИО, должность, номер телефона и т.д.

Данные в **LDAP-каталоге** строго структурированы и все атрибуты должны быть определены в схеме данных заранее. Перечень доступных для конкретного объекта атрибутов задается списком назначенных ему классов, см. атрибут ``objectClass``. Например, учетные записи пользователей могут содержать атрибут ``gidNumber`` по той причине, что им назначен класс объектов ``posixAccount``, см. :ref:`ldap-requests 3`.

.. figure:: media/рис3.png
   :name: ldap-requests 3

   Возможность указать gidNumber определяется наличием класса объектов posixAccount

Всем объектам каталога назначен, как минимум, один класс ``top``, т.к. в нем описан атрибут ``objectClass``, с помощью которого работает механизм назначения классов. Объекты с одним классом практически не используются, поэтому в каталоге у объектов два и более класса.

Для демонстрации ниже указан класс объектов ``posixGroup``, который определен в схеме следующим образом:

::

   ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Standard LDAP objectclass' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) X-ORIGIN 'RFC 2307' )

где:

   - ``1.3.6.1.1.1.2.2`` – это идентификатор объекта (object id, ``OID``). Глобальные идентификаторы присваиваются международными организациями (IANA, ISO, ITU-T, ANSI, BSI), а для расширения схемы в прикладных системах используется пространство номеров с префиксом ``1.3.6.1.4.1.X``, где ``X`` – это внутренний номер организации. Например, объекты компании X имеют префикс ``1.3.6.1.4.1.47836.``;

   - ``NAME '  '`` – инструкция NAME задает имя класса;

   - ``DESC '  '`` – инструкция DESC задает описание класса;

   - ``SUP '  '`` – инструкция SUP указывает родительский класс. В приведенном примере наследование идет от класса ``top``, поэтому объектам будет доступен его атрибут ``objectClass``;

   - ``STRUCTURAL`` – инструкция указывает, что класс относится к виду структурных. Существуют также абстрактные (ABSTRACT) и вспомогательные (AUXILIARY) классы, но в контексте автоматизации различия между видами классов не принципиальны;

   - ``MUST`` и ``MAY`` – инструкции, которые позволяют задать списки обязательных и дополнительных атрибутов. Полный перечень атрибутов, доступных объекту, расширяется атрибутами, которые наследуются от всех родительских классов;

   - ``X-ORIGIN '  '`` – инструкция, которая позволяет задать комментарий с ссылкой на документацию, из которой можно почерпнуть дополнительную информацию об этом классе. В приведенном примере информацию следует искать в документе RFC 2307.

Один и тот же атрибут может быть использован в нескольких классах, поэтому пользователям можно задать значение ``gidNumber``, т.к. им назначен класс ``posixAccount``, и, в то же время, он может быть задан у групп, т.к. им назначен класс ``posixGroup``, см. :ref:`ldap-requests 4`.

.. figure:: media/рис4.png
   :name: ldap-requests 4

   Использование атрибута gidNumber классами posixAccount и posixGroup

Подробное описание атрибута ``gidNumber``:

::

   ( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'Standard LDAP attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'RFC 2307' )

где:

   - ``1.3.6.1.1.1.1.1`` – это уникальный идентификатор атрибута;

   - ``NAME '  '`` – инструкция NAME задает атрибута;

   - ``DESC ' '`` – инструкция DESC задает описание атрибута;

   - ``SYNTAX '  '`` – инструкция задает тип хранимого в атрибуте значения. В приведенном примере ``1.3.6.1.4.1.1466.115.121.1.27`` соответствует целым числам. Описание всех типов данных можно найти в RFC4517;

   - ``SINGLE-VALUE`` – инструкция указывает, что атрибут может хранит только одно значение. Если этой инструкции не будет, то объектам можно будет присваивать несколько значений этого атрибута;

   - ``X-ORIGIN '  '`` – инструкция, которая позволяет задать комментарий с ссылкой на документацию.

Описание схемы данных хранится в файлах на диске в директориях:

   - ``/usr/share/dirsrv/schema/``

   - ``/etc/dirsrv/schema/``

   - ``/usr/share/dirsrv/updates/``

   - ``/etc/dirsrv/slapd-ALD-COMPANY-LAN/``

При обращении к каталогу по **LDAP** информацию можно получить из операционного **DIT** ``cn=schema``.

Взаимодействие с каталогом через LDAP-протокол
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Графическое приложение для работы с LDAP-каталогом (Apache Directory Studio)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Для работы с **LDAP-каталогом** из графического интерфейса: просмотра структуры каталога, редактирования записей, импорта и экспорта данных – можно воспользоваться таким бесплатным инструментом, как Apache Directory Studio, см. :ref:`ldap-requests 5`. Загрузить приложение можно с `официального сайта <directory.apache.org/studio/>`_, для работы потребуется ``java runtime``.

.. figure:: media/рис5.png
   :name: ldap-requests 5

   Apache Directory Studio

Настройка соединения
''''''''''''''''''''''''''''''''''

Чтобы подключиться к **LDAP-каталогу** нужно создать новое соединение через меню **LDAP** > **New connection**. Откроется окно для создания нового подключения см. :ref:`ldap-requests 6`.

.. figure:: media/рис6.png
   :name: ldap-requests 6

   Настройка сети для нового LDAP-подключения 

.. note:: 

   По умолчанию Apache Directory Studio предлагает создать незащищенное подключение на порт 389, что допустимо только при обращении к каталогу по ``localhost``, т.е. приложение должно быть установлено непосредственно на контроллер домена, т.к. при подключении пароль будет передаваться в открытом виде. В случае подключения к каталогу с клиента, обязательно использование порта 636 и метода шифрования SSL, чтобы перехват пароля был невозможен.

Аутентификация по паролю называется связыванием (Bind). Подключение к каталогу доступно с правами супер-пользователя ``cn=Directory Manager`` или доменного администратора **ALD Pro** ``uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan``, см. :ref:`ldap-requests 7`. В зависимости от учетной записи права на доступ к записям и атрибутам разнятся.

.. note:: 

   Сразу после установки системы пароли этих учетных записей совпадают. Чтобы сбросить пароль Directory Manager потребуется вручную менять хэш, записанный в файле *dse.ldif*, в строке, начинающейся с ``nsslapd-rootpw``.

Перед закрытием окна следует проверить корректность подключения нажатием кнопки **Check Authentication**.

.. figure:: media/рис7.png
   :name: ldap-requests 7

   Настройка аутентификации для нового LDAP-подключения

После добавления подключения новый сервер появится в списке **Connections**. Для подключения к серверу нужно дважды кликнуть по новой записи.

Просмотр и экспорт объектов каталога
''''''''''''''''''''''''''''''''''''''''''''''''''

Для просмотра записи в древе каталога следует выбрать нужный **RDN** из окна **LDAP browse**. Например, при нажатии на пункт ``cn=accounts`` из списка слева, основные атрибуты отобразятся в центральном окне с именем ``cn=accounts,dc=ald,dc=company,dc=lan``. См. :ref:`ldap-requests 8`.

.. figure:: media/рис8.png
   :name: ldap-requests 8

   Окно LDAP browse

Также можно открыть любой **DN** из меню **Navigate** > **Go to DN**, например ``cn=config``, но для его просмотра нужна учетная запись ``cn=Directory Manager``, см. :ref:`Полезные DN`.

.. list-table:: Полезные DN
   :widths: 50 50
   :header-rows: 1
   :class: longtable

   * - Запись DN
     - Описание
   * - cn=accounts,dc=ald,dc=company,dc=lan
     - Контейнер который содержит дочерние записи контейнеров учетных записей, компьютеров, групп и д.р.
   * - cn=computers,cn=accounts,...
     - Содержит список компьютеров в домене
   * - cn=dns,...
     - Содержит информацию о записях DNS
   * - cn=groups,cn=accounts,...
     - Содержит список групп пользователей
   * - cn=hostgroups, cn=accounts,...
     - Содержит группы компьютеров домена, например ipaservers
   * - cn=orgunits,cn=accounts,...
     - Содержит список подразделений, которые отображаются у других записей в атрибуте rbtadp, например у пользователя или компьютера
   * - cn=users,cn=accounts,...
     - Содержит список пользователей домена

Экспорт объектов производится через контекстное меню, кликом по требуемой записи, например, ``cn=users`` в дереве см. :ref:`ldap-requests 9`, а затем по **Export** с дальнейшим выбором формата.

.. figure:: media/рис9.png
   :name: ldap-requests 9

   Окно LDAP browse

После выбора формата **CSV** откроется диалог настроек параметров экспорта данных, см. :ref:`ldap-requests 10`. В окне параметров настройте нужные фильтры и список требуемых атрибутов для вывода.

.. figure:: media/рис10.png
   :name: ldap-requests 10

   Окно параметров CSV Export

Следующим шагом укажите имя файла, в который вы хотите записать **CSV**, см. :ref:`ldap-requests 11`, а затем нажмите **Finish**.

.. figure:: media/рис11.png
   :name: ldap-requests 11

   Окно CSV Export выбор имени файла

Если нажать на ссылку **Text Formats** см. :ref:`ldap-requests 12`, откроются настройки генерации текстовых данных, таких как обрамление данных кавычками, разделитель строк и др.

.. figure:: media/рис12.png
   :name: ldap-requests 12

   Настройки вывода текстового форматирования

Результат выполнения экспорта данных в программе **LibreOffice Calc** - см. :ref:`ldap-requests 13`.

.. figure:: media/рис13.png
   :name: ldap-requests 13

   Результат выполнения экспорта данных

Этот файл можно использовать для автоматизации в качестве входной информации или для подготовки отчета по объектам из каталога **LDAP**.

Просмотр схемы каталога
'''''''''''''''''''''''''''''''''''''

Для просмотра схемы каталога следует выбрать **Root DSE** в дереве **LDAP Browser** и выполнить команду **LDAP** > **Open Schema Browser** см. :ref:`ldap-requests 14`.

.. figure:: media/рис14.png
   :name: ldap-requests 14

   Меню Open Schema Browser

По умолчанию в **Schema Browser** открывается страница для просмотра классов объектов ``(Object Classes)``, подробной информации ``(Details)`` и поиска. Например, по фильтру ``posix`` выводятся упоминаемые ранее классы ``posixAcccount`` и ``posixGroup``, см. :ref:`ldap-requests 15`.

.. figure:: media/рис15.png
   :name: ldap-requests 15

   Schema Browser и вкладка Object Classes

Для перехода к другим группам объектов предназначены ярлычки в нижней части вкладки. Страница **Attribute Types** предназначена для просмотра атрибутов, поиска по списку всех атрибутов и просмотра деталей выбранного атрибута. Например, поиск по фильтру ``gid`` выводит несколько атрибутов, в именах которых содержится эта подстрока, см. :ref:`ldap-requests 16`.

.. figure:: media/рис16.png
   :name: ldap-requests 16

   Schema Browser и вкладка Attribute Types

Утилиты для работы с LDAP-каталогом
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Для работы с **LDAP-каталогом** из командной строки используются утилиты пакета ``ldap-utils``:

- ``ldapwhoami`` - выполняет подключение к каталогу и возвращает имя текущего пользователя;
- ``ldapsearch`` - выполняет поиск по каталогу с указанными параметрами;
- ``ldapadd`` - позволяет создать новый объект в каталоге;
- ``ldapdelete`` - позволяет удалить объект из каталога;
- ``ldapmodify`` - позволяет изменить атрибуты существующего объекта в каталоге;
- ``ldapcompare`` - позволяет сравнить текущее значение атрибута конкретной записи с желаемым значением и получить результат в формате TRUE/FALSE;
- ``ldapexop`` - позволяет выполнять расширенные операции, добавленные разработчиками конкретного **LDAP-сервера**. Расширенные операции подобны хранимым процедурам SQL, и позволяют расширять возможности взаимодействия с сервером без внесения изменений в **LDAP-протокол**;
- ``ldappasswd`` - позволяет сбросить пароль для учетной записи;
- ``ldapmodrdn`` - позволяет переименовывать существующие объекты каталога.

Подключение к LDAP
''''''''''''''''''''''''''''''''

Подключение к **LDAP-каталогу** происходит в рамках вызова каждой утилиты. Допустимо задавать параметры подключения в явном виде или полагаться на настройки по умолчанию, которые описаны в файле */etc/ldap/ldap.conf*. При вводе компьютера в домен указанный файл настраивается автоматически, подключение будет выполняться к одному из контроллеров домена по протоколу **LDAPS** с использованием **Kerberos-билета** из связки ключей.

Проверка доступа в каталог осуществляется командой **ldapwhoami**:

.. code-block:: bash

   ldapwhoami

Результат выполнения:

.. code-block:: console

   SASL/GSSAPI authentication started
   ldap_sasl_interactive_bind_s: Local error (-2)
           additional info: SASL(-1): generic failure: GSSAPI Error: Unspecified GSS failure.  Minor code may provide more information (No Kerberos credentials available (default cache: KEYRING:persistent:1000))

Данный результат сообщает об отсутствии учетных записей в кэше Kerberos. Поэтому следует выполнить вход ``kinit admin`` и проверить доступ повторно **ldapwhoami**:

.. code-block:: bash

   kinit admin
   ldapwhoami

Результат выполнения:

.. code-block:: console

   Password for admin@ALDPRO.DOMAIN:
   SASL/GSSAPI authentication started
   SASL username: admin@ALDPRO.DOMAIN
   SASL SSF: 256
   SASL data security layer installed.
   dn: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan

Наличие TGT-Kerberos билета позволяет получить сервисный билет на доступ к **LDAP-серверу**. Проверить текущие билеты в кэше можно командой ``klist``:

.. code-block:: console

   klist

Результат выполнения:

.. code-block:: console

   Ticket cache: KEYRING:persistent:1000:1000
   Default principal: admin@ALD.COMPANY.LAN
    
   Valid starting       Expires              Service principal
   22.05.2023 09:58:25  23.05.2023 09:58:01  ldap/dc-1.ald.company.lan@ALD.COMPANY.LAN
   22.05.2023 09:58:08  23.05.2023 09:58:01  krbtgt/ALD.COMPANY.LAN@ALD.COMPANY.LAN

В кеше присутствует билет **LDAP**: ``ldap/dc-1.ald.company.lan@ALD.COMPANY.LAN``, благодаря этому возможно использование утилиты из пакета ``ldap-utils``.

При подключении к серверу можно переопределять любой из параметров по умолчанию, задавая их в явном виде:

.. code-block:: console

   ldapsearch -H dc-1.ald.company.lan -ZZ -x -W -D "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan" -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" '(uid=*)' uid givenName sn

где:

- **Параметр** ``-H`` - для указания адреса **LDAP-сервера**, например ``dc-1.ald.company.lan``. Параметр позволяет указать нешифрованный протокол **ldap** или шифрованный **ldaps**, например, ``ldap://dc-1.ald.company.lan``. Подключение к **LDAP-каталогу** возможно также через unix-сокет, если приложение выполняется на том же сервере. В этом случае следует указать ``ldapi://%2fvar%2frun%2fslapd-ALD-COMPANY-LAN.socket``.

- **Параметр** ``-ZZ`` - это параметр включения шифрованного соединения, где первая **Z** означает отправку серверу запроса ``STARTTLS``. Если сервер не поддерживает TLS, соединение продолжится, и оно не будет шифроваться. Вторая **Z** устанавливает требование использовать только шифрованное соединение, если сервер не поддерживает TLS, соединение прервется. Рекомендуется использовать двойной ключ ``-ZZ`` для установления шифрованного соединения, если по каким-либо причинам 636-порт недоступен, а также требование использовать безопасный протокол TLS на 636-порту можно задать с помощью ключа ``-H``, указав порт явно ``ldaps://dc-1.ald.company.lan``;

- **Параметр** ``-x`` - указывает на необходимость выполнить простую аутентификацию по логину/паролю;

- **Параметр** ``-D`` - задает **Bind DN** пользователя для аутентификации, например, ``uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan`` или супер-пользователя, ``cn=Directory Manager``;

- **Параметр** ``-W`` - указывает, что пароль должен быть предоставлен в интерактивном режиме (а если нужно передать пароль в параметре, то это можно сделать с помощью ключа ``-w``);

Используя параметры ``-D`` и ``-W``, доступно подключение к нужному серверу по IP или имени сервера и файл конфигурации задействован не будет.

Поиск по каталогу ldapsearch
''''''''''''''''''''''''''''''''''''''''''

Для поиска информации можно использовать утилиту **ldapsearch**, которая извлекает из каталога набор записей с указанными атрибутами в соответствии с заданными критериями фильтрации и заданного списка атрибутов. Синтаксис команды в общем виде выглядит следующим образом:

.. code-block:: console

   ldapsearch [options] [filter [attributes…]]

где:

- ``options`` - параметры вызова, в т.ч. параметры подключения. Для простоты использованы параметры подключения по умолчанию;
- ``filter`` - служит для точного указания критериев поиска;
- ``attributes`` - указывает, какие атрибуты необходимо запросить.

Далее приведен пример запроса, который должен выдавать список всех пользователей домена из ``cn=users``, которая в свою очередь является потомком записи ``cn=accounts``, и все они находятся в пространстве корневого суффикса ``dc=ald,dc=company,dc=lan``. Таким образом полным уникальным именем записи (Distinguished Name, **DN**), в которой хранятся пользователи, является ``cn=users,cn=accounts,dc=ald,dc=company,dc=lan``.

Поиск по каталогу с помощью команды **ldapsearch**:

.. code-block:: console

   ldapsearch -Q -s sub -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" '(uid=*)' uid givenName sn

где:

- Параметр ``-Q`` – это тихий режим SASL, не выводится информация о SASL-подключении, который полезен при автоматизации для очистки данных от технической информации.

- Параметр ``-s`` – это область поиска (scope). Может принимать следующие значения:
   - ``one`` - поиск идет по дочерним записям на один уровень ниже в иерархии;
   - ``sub`` - поиск идет по всем дочерним записям на всю глубину иерархии, параметр по умолчанию;
   - ``children`` - то же, что и ``sub``, но ограничивает поиск только дочерними записями;
   - ``base`` - ограничивает поиск по текущей записи, заданной параметром ``-b``. Если задан ``one``, поиск идет по дочерним записям на один уровень ниже в иерархии. Если задан ``sub``, то поиск идет по всем дочерним записям на всю глубину иерархии, начиная с записи, заданной параметром ``-b``, при этом включая саму базовую запись. ``children`` - то же, что и ``sub``, но ограничивает поиск только дочерними записями, не включая базовую запись.

- Параметр ``-b`` – это базовая запись (base), которая будет использоваться в качестве начальной точки для поиска по древу.

- Параметр ``'(uid=\*)'`` – это фильтр, в котором осуществлен поиск всех записей, имеющих атрибут ``uid``. Фильтры и составные фильтры рассматриваются далее.

- Параметры ``uid givenName sn`` – это атрибуты, которые нужно вывести в результат. Если их не указывать, то будут отображены все атрибуты найденных записей.

Результат поиска по каталогу:

.. code-block:: console

   ### extended LDIF
   #
   ### LDAPv3
   ### base <cn=users,cn=accounts,dc=ald,dc=company,dc=lan> with scope subtree
   ### filter: (uid=*)
   ### requesting: uid givenName sn
   #
    
   ### admin, users, accounts, ald.company.lan
   dn: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   uid: admin
   sn: Administrator
    
   ### petrovp, users, accounts, ald.company.lan
   dn: uid=petrovp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   uid: petrovp
   givenName:: 0J/RkdGC0YA=
   sn:: 0J/QtdGC0YDQvtCy
    
   ### ivani, users, accounts, ald.company.lan
   dn: uid=ivani,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   uid: ivani
   givenName:: 0JjQstCw0L0=
   sn:: 0JjQstCw0L3QvtCy
    
   ### search result
   search: 4
   result: 0 Success
    
   ### numResponses: 4
   ### numEntries: 3

Результат выводится в поток ``stdout`` в текстовом формате **LDIF** и его можно использовать по конвейеру ``pipeline`` или перенаправить в файл для дальнейшей обработки. Подробнее о формате **LDIF** см. в разделе 2.2.3.

Существуют операционные атрибуты (operational attributes), которые встроены в **LDAP-сервер** и управляются им самим, например, ``entrydn``, ``entryid``, ``parentid`` или ``nsAccountLock``. Большинство операционных атрибутов для пользователей доступны только для чтения. Чтобы их увидеть, необходимо указать ``“+”`` в конце команды, однако не все операционные атрибуты доступны для просмотра таким образом:

.. code-block:: bash

   ldapsearch -Q -LLL -s base -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "+"

Результат выполнения:

.. code-block:: console

   dn: cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   creatorsName: cn=Directory Manager
   modifiersName: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   createTimestamp: 20230324160645Z
   modifyTimestamp: 20230324161206Z
   nsUniqueId: df91d884-ca5d11ed-b5f0ee72-e6acffe1
   parentid: 2
   entryid: 3
   entryusn: 6055
   numSubordinates: 6

Например, из результата видно, что запись ``cn=users`` имеет операционный атрибут ``numSubordinates``, который показывает число дочерних записей.

Фильтры запроса
"""""""""""""""

Поиск **ldapsearch** можно сделать более гибким с помощью фильтров, задав условие для отбора нужной информации. Синтаксис фильтра выглядит следующим образом:

.. code-block::
   
   (attribute operator value)

В таблице ниже приведены операторы ``(operator)``, используемые в фильтрах:

.. _tab:criteria:
.. list-table::
   :widths: 25 75
   :header-rows: 1
   :class: longtable
   :name: Операторы, используемые в фильтрах

   * - Оператор
     - Комментарий
   * - ``>=``
     - Больше или равно. Возвращает результат, если атрибут больше или равен какому-то значению, например (uidNumber>=180003)
   * - ``<=``
     - Меньше или равно. Возвращает результат, если атрибут меньше или равен какому-то значению, например: (uidNumber<=180003)
   * - ``=*``
     - Наличие. Возвращает результат, если атрибут содержит одно или более значений, например: (cn=*)
   * - ``=``
     - Равенство. Возвращает результат, если атрибут равен некоторому значению, например: (sn=petrov)
   * - ``~=``
     - Примерное равенство. Возвращает результат, если атрибут содержит приблизительно схожее значение, например: (cn~=petrof)
   * - ``=string*string``
     - Включает в себя. Возвращает значение, если атрибут содержит определенную подстроку. Где знак "*" означает ноль или более символов, например: (cn=pet*ov)

Например, чтобы вывести список пользователей с фамилией ``petrov``, потребуется указать фильтр по атрибуту ``sn`` (surname):

.. code-block:: bash

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(sn=Петров)" cn

Результат выполнения:

.. code-block:: console

   dn: uid=ppetrov,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   cn: Petr Petrov

В результате запрос отфильтрован по атрибуту ``sn``. Таким образом можно подготовить любые данные для обработки.

Составные фильтры запроса
"""""""""""""""""""""""""

Для большей гибкости можно использовать несколько фильтров, объединяя их с помощью логических операторов. Синтаксис составного фильтра выглядит следующим образом:

.. code-block:: console

   (Boolean-operator(filter)(filter)(filter)...)

Ниже приведены логические операторы ``(Boolean-operator)``, используемые в составных фильтрах:

- \& - Логическое И. Фильтр возвращает те записи, которые удовлетворяют всем указанным условиям, например: ``(&(uid=user)(uidNumber=180003))``
- \| - Логическое ИЛИ. Фильтр возвращает те записи, которые удовлетворяют одному из указанных условий, например:
- \! - Логическое НЕ. Фильтр возвращает те записи, которые не удовлетворяют указанному условию, например: ``(!(uid=admin))``

Например, поиск пользователей из группы ``admins``, которые два и более месяца не меняли пароль, выглядит так:

.. code-block:: console

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(&(memberof=cn=admins*)(krbLastPwdChange>=$(date +"%Y%m%d000000Z" -d "-60 days")))" cn krbLastPwdChange

Результат выполнения:

.. code-block:: console

   dn: uid=admin,cn=users,cn=accounts,
   cn: Administrator
   krbLastPwdChange: 20230324160900Z

В результате запроса, выведен список администраторов, у которых пароль изменялся за последние 60 дней. Через конструкцию можно добавлять подкоманды: ``$(date +“%Y%m%d000000Z” -d “-60 days”)`` между двойными кавычками фильтра. Так, например, можно вычислить дату и подставить ее в фильтр в нужном формате.

Получение схемы объектных классов
"""""""""""""""""""""""""""""""""

Объектные классы описаны в операционном атрибуте ``objectClasses`` из специальной записи схемы ``cn=schema``. Посмотреть список всех классов можно командой:

.. code-block:: console

   ldapsearch -Q -LLL -o ldif-wrap=no -s base -b "cn=schema" objectClasses

Результат выполнения:

.. code-block:: console

   dn: cn=schema
   objectClasses: ( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass X-ORIGIN 'RFC 45
    12' )
   objectClasses: ( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectNam
    e X-ORIGIN 'RFC 4512' )
   objectClasses: ( 2.5.20.1 NAME 'subschema' AUXILIARY MAY ( dITStructureRules $
     nameForms $ dITContentRules $ objectClasses $ attributeTypes $ matchingRules
     $ matchingRuleUse ) X-ORIGIN 'RFC 4512' )
   objectClasses: ( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' SUP top
    AUXILIARY X-ORIGIN 'RFC 4512' )
   objectClasses: ( 2.5.6.11 NAME 'applicationProcess' SUP top STRUCTURAL MUST cn
     MAY ( seeAlso $ ou $ l $ description ) X-ORIGIN 'RFC 4519' )
   ...

В результате выведется список всех классов, в котором можно искать через команду ``grep``:

.. code-block:: console

   ldapsearch -Q -LLL -o ldif-wrap=no -s base -b "cn=schema" objectClasses | grep --color posix

Результат выполнения:

.. code-block:: console

   objectClasses: ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Standard LDAP objectclass' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) X-ORIGIN 'RFC 2307' )
   objectClasses: ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Standard LDAP objectclass' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) X-ORIGIN 'RFC 2307' )
   objectClasses: ( 2.16.840.1.113730.3.2.326 NAME 'dynamicGroup' DESC 'Group containing internal dynamically-generated members' SUP posixGroup AUXILIARY MAY dsOnlyMemberUid X-ORIGIN 'Red Hat Directory Server' )

Получение схемы атрибутов
"""""""""""""""""""""""""

Также, как и объектные классы, атрибуты описаны через операционный атрибут ``attributeTypes`` корневой записи схемы ``cn=schema``. Посмотреть список всех атрибутов можно командой:

.. code-block:: console

   ldapsearch -Q -LLL -o ldif-wrap=no -s base -b "cn=schema" attributeTypes

Результат выполнения:

.. code-block:: console

   dn: cn=schema
   attributeTypes: ( 2.16.840.1.113730.3.1.582 NAME 'nsDS5ReplicaCredentials' DES
    C 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 SINGL
    E-VALUE X-ORIGIN 'Netscape Directory Server' )
   attributeTypes: ( 2.16.840.1.113730.3.8.22.1.2 NAME 'ipaCertMapMapRule' DESC '
    Certificate Mapping Rule' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X
    -ORIGIN 'IPA v4.5' )
   attributeTypes: ( 1.3.6.1.4.1.15953.9.1.1 NAME 'sudoUser' DESC 'User(s) who ma
    y  run sudo' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SY
    NTAX 1.3.6.1.4.1.1466.115.121.1.26 X-ORIGIN 'SUDO' )
   ...

Результат команды также можно перенаправить в файл командой перенаправления ``>``:

.. code-block:: console

   ldapsearch -Q -LLL -o ldif-wrap=no -s base -b "cn=schema" attributeTypes > attrs

Формат данных LDIF
''''''''''''''''''''''''''''''''

Как упоминалось ранее, утилита **ldapsearch** возвращает результаты в формате **LDIF**, **LDAP Data Interchange Format** – это формат представления записей службы каталогов или их изменений в текстовой форме. Записи каталога или их изменения представляются набором **LDIF-записей**, по одной на каждую запись каталога или изменение. **LDIF** был разработан в начале 90-х годов и был доработан для использования с **LDAPv3**. Описание формата опубликовано в RFC 4525. Пример структуры **LDIF**:

| *dn: уникальное имя*
| *имя атрибута: значение атрибута*
| *имя атрибута:: base64 значение атрибута*

| *dn: уникальное имя*
| *имя атрибута: значение атрибута*
| *имя атрибута:< значение атрибута url*

Записи каталога представляются группами строк, разделенных пустой строкой, при этом каждая строка в группе представляет отдельное значение атрибута записи. Первая строка в группе должна представлять уникальное имя записи **DN**. Значение атрибута записывается в кодировке ASCII и отделяется от его имени символом ``:``. Значения, не подходящие под эту кодировку, записываются в кодировке base64 и отделяются от имени атрибута символами ``::``. Данный формат можно использовать для хранения данных, добавления и модификации записей в каталоге. Пример **LDIF-файла** добавления записи в каталог нового подразделения ``add_dp.ldif``:

.. code-block:: console

   dn: ou=marketing,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   objectClass: rbta-org-unit
   objectClass: top
   ou: marketing
   displayName: Маркетинг

.. note:: 

   В конце **LDIF** стоит пустая строка, которая определяет разделение между записями. Даже если запись одна, нужно ставить разделитель из пустой строки.

При модификации используется директива ``changetype``, которая может принимать значения: ``add``, ``modify``, ``replace``, ``delete`` и ``moddn``, более подробно рассмотренные далее в примере **ldapmodify**.

Добавление записи в каталог через ldapadd
'''''''''''''''''''''''''''''''''''''''''''''''''''''''

Добавление в каталог информации возможно утилитами ``ldapadd`` и ``ldapmodify``, где ``ldapadd`` это символическая ссылка на команду ``ldapmodify``, поэтому можно использовать ``ldapmodify -a``. Добавьте новое подразделение через описанный выше **LDIF-файл** ``add_dp.ldif`` командой перенаправления ``<``:

.. code-block:: console

   Ldapadd -Q < add_dp.ldif

Результат выполнения:

.. code-block:: console

   adding new entry "ou=marketing,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan"

В результате выводится сообщение ``adding new entry`` и **DN** новой записи.

Второй способ добавления – это работа с утилитой в интерактивном режиме для этого используется команда ``ldapadd``:

.. code-block:: console

   ldapadd -Q

Введите текст записи в формате **LDIF**, в конце которого должна быть пустая строка:

.. code-block:: console

   dn: ou=develop,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   objectClass: rbta-org-unit
   objectClass: top
   ou: develop
   displayName: Разработка ПО

Мигающий курсор означает ожидание ввода символов или сигналов. Отправьте терминалу сигнал EOF командой ``Ctrl+``.

.. code-block:: console

   Ctrl + <d>

Результат выполнения:

.. code-block:: console

   objectClass: rbta-org-unit
   objectClass: top
   ou: Develop
   displayName: Разработка ПО
   adding new entry "ou=Develop,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan"

Программа сообщит об успешном добавлении ``adding new entry``. Для выхода без изменений отправьте другой сигнал ``SIGINT Ctrl +``. Использовать интерактивный режим для скриптов не рекомендуется, потому что может произойти событие ожидания ввода и скрипт зависнет, ожидая ввода пользователя. Поэтому в написании скриптов для команд используют параметр ``-y`` для подтверждения всех запросов. В примерах далее использован первый подход с перенаправлением.

.. _ref_auto_adm_ldap_modification_data:

Модификация записей в каталоге через ldapmodify
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Как говорилось ранее, директива ``changetype`` должна следовать сразу после ``dn``. Это нужно для того, чтобы утилита понимала, какая запись изменяется. Если в **LDIF** отсутствует директива ``changetype``, то по умолчанию подразумевается ``changetype: add`` - добавление записи в каталог.

Директивы changetype: modify и add
""""""""""""""""""""""""""""""""""

Данная директива **LDIF** позволяет добавить атрибут к записи, но есть атрибуты, которые могут быть только в единичном числе, например, атрибут ``displayName``. В этом случае при добавлении второго атрибута возникнет ошибка ``ldap_add: Already exists (68)``. В примере ниже пользователю ``admin`` добавляется новая локация - атрибут ``L`` в файл *add_loc.ldif*:

.. code-block:: console

   dn: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   add: l
   l: Казань
   \n

Пустая строка (\n) - это разделитель между записями. Ее необходимо добавлять даже при операциях с одной записью. Запуск команды модификации **ldapmodify**:

.. code-block:: console

   ldapmodify -Q < add_loc.ldif

Также ее можно передать по конвейеру:

.. code-block:: console

   cat add_loc.ldif  | ldapmodify -Q

Результат выполнения:

.. code-block:: console

   modifying entry "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Директивы changetype: modify и replace
""""""""""""""""""""""""""""""""""""""

Для перемещения пользователя в другой департамент ему нужно изменить атрибут ``rbtadb``. Сначала создается файл, например *changedp.ldif*:

.. code-block:: console

   dn: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   replace: rbtadp
   rbtadp: ou=marketing,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan

А затем производится запуск команды модификации **ldapmodify**:

.. code-block:: console

   ldapmodify -Q < changedp.ldif

Результат выполнения:

.. code-block:: console

   modifying entry "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Успешное изменение записи можно проверить в Apache Directory Studio.

Директивы changetype: modify и delete
"""""""""""""""""""""""""""""""""""""

Если необходимо изменить несколько атрибутов у записи, то можно несколько операций разделить c новой строки символом ``-``. Пример изменений нескольких атрибутов удаляет атрибут ``l: Казань`` и добавляет ``l: Москва``. Сначала создается файл, например *change_two_attrs.ldif*:

.. code-block:: console

   dn: uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   delete: l
   l: Казань
   -
   add: l
   l: Москва

А затем производится запуск команды модификации **ldapmodify**, указав файл:

.. code-block:: console

   change_two_attrs.ldif через параметр -f:
   ldapmodify -Q -f change_two_attrs.ldif

Результат выполнения:

.. code-block:: console

   modifying entry "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Директивы changetype: modrdn и newrdn
"""""""""""""""""""""""""""""""""""""

Директива ``modrdn`` изменяет **RDN** записи, другими словами переименовывает запись. Например, переименуйте подразделение Разработки ПО ``ou=develop`` на ``ou=arch`` архитекторов через файл ``rename_rdn.ldif``, в котором директива ``deleteoldrdn: 1`` удалит старый **DN**:

.. code-block:: console

   dn: ou=develop,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modrdn
   newrdn: ou=arch
   deleteoldrdn: 1
    
   dn: ou=arch,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   replace: displayName
   displayName: Архитекторы

Перенапрвление файла с помощью команды **ldapmodify**:

.. code-block:: console

   rename_rdn.ldif:
   ldapmodify -Q < rename_rdn.ldif

Результат выполнения:

.. code-block:: console

   modifying rdn of entry "ou=develop,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan"
    
   modifying entry "ou=arch,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan"

.. important:: 

   При использовании директивы ``modrdn`` есть угроза возникновения ошибки в виде некорректного количества записей (``total``).

Директива changetype: delete
""""""""""""""""""""""""""""

Если необходимо удалить запись из каталога, используется директива ``changetype: delete``, однако, у записи не должно быть ни одной дочерней записи. Например, удаление группы ``ou=arch`` через файл ``del_ou.ldif``:

.. code-block:: console

   dn: ou=arch,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: delete

Проверка результата:

.. code-block:: console

   ldapmodify -Q < del_ou.ldif

Результат выполнения:

.. code-block:: console

   deleting entry "ou=arch,ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan"

Если запись будет не найдена, то выведется сообщение об ошибке:

.. code-block:: console

   ldap_delete: No such object (32)
           matched DN: ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan

Работа с кириллицей и Unicode
'''''''''''''''''''''''''''''''''''''''''''

Если атрибуты содержат значения не в ASCII-кодировке, то утилита **ldapsearch** при выводе значений представляет их в base64 кодировке. В примере, при создании пользователя ``ppetrov`` атрибуту ``displayName`` было присвоено значение ``Петров П.П.``:

.. code-block:: console

   ldapsearch -Q -LLL -s base -b "uid=ppetrov,cn=users,cn=accounts,dc=ald,dc=company,dc=lan" displayname

Результат выполнения:

.. code-block:: console

   dn: uid=petrov.pp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   displayname:: 0J/QtdGC0YDQvtCyINCfLtCfLg==

Поскольку **ldapsearch** понимает только ASCII-символы, она представила значение ``displayName`` в неудобной для чтения base64 строке. Увидеть исходное значение можно только сделав обратное преобразование:

.. code-block:: console

   echo '0J/QtdGC0YDQvtCyINCfLtCfLg==' | base64 -d

Результат выполнения:

.. code-block:: console

   Петров П.П.

Для того, чтобы получить результат сразу в удобном для автоматизации виде, можно использовать команду:

.. code-block:: console

   ldapsearch -Q -LLL -s base -b "uid=ppetrov,cn=users,cn=accounts,dc=ald,dc=company,dc=lan" displayname  | perl -MMIME::Base64 -Mutf8 -pe 's/^([-a-zA-Z0-9;]+):(:\s+)(\S+)$/$1.$2.&decode_ba    se64($3)/e'

Результат выполнения:

.. code-block:: console

   dn: uid=petrov.pp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   displayName: Петров П.П.

Производить какие-то дополнительные действия в фильтрах при поиске по строкам, не содержащим ASCII-символы, не требуется:

.. code-block:: console

   ldapsearch -Q -LLL -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan"  "(displayName=Петров П.П.)" cn

Результат выполнения:

.. code-block:: console

   dn: uid=petrov.pp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   cn: Petr Petrov

Примеры автоматизации
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

В предыдущих разделах объясняется, как можно управлять каталогом через **LDAP-протокол**, это своего рода «кирпичики», из которых можно «строить дома», т.е. решать сложные задачи автоматизации. Ниже представлены примеры на языках bash и python, а также работа с форматами **CSV**, **LDIF**, **JSON**.

Работа с объектами из командной строки bash
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

В операционных системах Microsoft Windows для решения задач автоматизации раньше использовали командную оболочку и интерпретатор командной строки CMD. На замену ему пришел более мощный объектно-ориентированный PowerShell, который предоставляет удобные инструменты для работы с датами, хэш-таблицами, словарями, **CSV** и **JSON** объектами, а также имеет множество дополнительных модулей, например, для взаимодействия с каталогом **Active Directory**, объектной моделью офисных приложений и пр.

В мире Linux есть множество оболочек и интерпретаторов, например, sh, bash, zsh и др., каждый из которых предлагает свой вариант синтаксиса и может вызывать утилиты, доступные в операционной системе, например, ls, ping, sed, cat, cut и др. Но все они в какой-то степени являются эквивалентом CMD, т.к. работают только с текстовыми переменными и не поддерживают объекты.

Обрабатывать текстовый поток, получаемый от других приложений, средствами командной строки довольно сложно, поэтому bash целесообразно дополнить возможностями языка программирования Python, который в мире Linux можно считать эквивалентом PowerShell, по крайней мере, в части удобства работы с объектами. Далее приведены примеры скриптов Python для конвертации **LDIF** в **JSON** / **CSV**, и способы работы с этими данными с помощью утилит ``jq`` / ``cut``.

Конвертер ldif2csv и генерация отчетов
'''''''''''''''''''''''''''''''''''''''''''''''''''

Обрабатывать **LDIF** довольно сложно, т.к. данные представлены в блоках строк, а нужные атрибуты могут отсутствовать. Вот пример Python скрипта, который позволяет конвертировать поток **LDIF** в **CSV**, для его использования нужно создать файл, например *ldif2csv.py*, и скопировать туда следующее:

.. code-block:: python

   #!/usr/bin/python3
   import sys
   import base64
   import re
    
   if sys.version_info[0] < 3:
     raise Exception("Use Python 3: python3 ldif2csv.py")
    
   ### read from sdtin
   data = sys.stdin.readlines()
   header_string = ""
   atrcheck = ""
   entry = ""
   dic_entries = {}
   dic_entry = {}
   current_dn = ""
   val_base64 = False
   max_size_cell = 32000  ### limit chars in excel cell
   ### main loop for parse headers and collect dict
   for line in data:
     ln = line.replace("\n", "").replace("\r", "")
     if len(ln) == 0:
       if not current_dn == "":
         dic_entries[current_dn] = dic_entry
         dic_entry = {}
         entry = ""
     else:
       if ln.startswith("version"):
         #dic_entries["version"] = ln.split(": ")[1]
         continue
       elif ln.lstrip()[0] == "#":
         continue
       elif ln[0] == " ":
         ### if line wrapped line starts with " " then add line to last attr
         dic_entry[atrcheck] += ln.lstrip()
         if val_base64:
           val_to_decode = dic_entry[atrcheck]
           try:
             dic_entry[atrcheck] = base64.b64decode(val_to_decode).decode(
               'utf-8').strip()
           except:
             dic_entry[atrcheck] = val_to_decode
         continue
       entry += ln
       attribute = []
       attribute_name = ""
       attribute_value = ""
       if ln.find(":: ") > 0:
         val_base64 = True
         attribute = ln.split(":: ")
         try:
           attribute_value = base64.b64decode(
             attribute[1]).decode('utf-8').strip()
         except:
           attribute_value = attribute[1]
       elif ln.find(":< ") > 0:
         val_base64 = False
         attribute = ln.split(":< ")
         attribute_value = attribute[1]
       else:
         val_base64 = False
         attribute = ln.split(": ")
         try:
           attribute_value = re.sub(r"^.*?: ", "", ln)
         except:
           attribute_value = ""
    
       atrcheck = attribute[0].replace(":", "")
       ### get attribute and check if attribute exist
    
       if dic_entry.get(atrcheck):
         new_len = len(dic_entry[atrcheck]) + len("|" + str(attribute_value))
         if new_len < max_size_cell:
           dic_entry[atrcheck] = str(
             dic_entry[atrcheck]) + "|" + str(attribute_value)
       else:
         dic_entry[atrcheck] = attribute_value
       if atrcheck == "dn":
         current_dn = attribute[1]
     if header_string.find(atrcheck) < 0:
       if header_string == "":
         header_string += atrcheck
       else:
         header_string += ";" + atrcheck
   ### add row if row not empty
   if entry != "":
     dic_entries[current_dn] = dic_entry
     dic_entry = {}
     entry = ""
    
   ### print utf-8-BOM and headers
   print('\ufeff' + "\"" + header_string.replace(";", "\";\"") + "\"")
   ### print data
   for d in dic_entries:
     od = dic_entries[d]
     csv_row = ""  ### row csv value
     split_char = ""  ### spliter for values
     for column in header_string.split(";"):
       if len(csv_row) > 0:
         split_char = ";"  ### need split because csv row have chars
       find_column = od.get(column)
       if find_column and find_column.strip():
         csv_row += split_char + "\"" + od[column].replace("\"", "\"\"") + "\""
       else:
         csv_row += split_char + "\"\""
     print(csv_row)

Пример конвертации данных из **ldapsearch** через конвейер:

.. code-block:: bash

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(uid=*)" uid displayName sn mail rbtadb | python3 ldif2csv.py

Результат выполнения:

.. code-block:: bash

   "dn";"uid";"sn";"displayName";"mail"
   "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"admin";"Administrator";"";""
   "uid=petrovp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"petrovp";"Петров";"Петров петр";"petrovp@ald.company.lan"
   "uid=ivani,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"ivani";"Иванов";"Иван Иванов";"ivani@ald.company.lan"
   "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"petrovss";"Сидоров";"Петр С.";"petrov.ss@ald.company.lan"

Сохранить этот вывод в файл *users.csv* можно простым перенаправлением:

.. code-block:: bash

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(uid=*)" uid displayName sn mail rbtadb | python3 ldif2csv.py > users.csv

На :ref:`ldap-requests 17` можно увидеть, как будет выглядеть содержимое файла, если его открыть в **LibreOffice Calc**.

.. figure:: media/рис17.png
   :name: ldap-requests 17

   Файл, экспортированный с помощью ldif2csv.py, открытый в LibreOffice Calc

Далее можно использовать полученный **CSV-файл** следующим образом:

.. code-block:: bash

   #!/bin/bash
   csv_file=$1
   cat $csv_file | while read line
   do
     if [ ! -z "${line}" ]; then
       if [[ "${line:0:1}" == "\"" ]]; then
        column1=$(echo ${line} | cut -d ";" -f 1 | sed "s/^\"//g" | sed "s/\"$//g")
        column2=$(echo ${line} | cut -d ";" -f 2 | sed "s/^\"//g" | sed "s/\"$//g")
        column3=$(echo ${line} | cut -d ";" -f 3 | sed "s/^\"//g" | sed "s/\"$//g")
        echo -e "$column1\t$column2\t$column3"
        ### можете добавить свои действия по обработке данных из файла
       fi
     fi
   done

Если сохранить приведенный скрипт в файл ``parse_csv.sh``, то ему можно передать имя **CSV-файла** для обработки, как параметр. А также, необходимо назначить скрипту права на выполнение:

.. code-block:: bash

   chmod +x ./parse_csv.sh
   ./parse_csv.sh users.csv

Результат выполнения:

.. code-block:: bash

   uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan       admin   Administrator
   uid=petrovp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan     petrovp Петров
   uid=ivani,cn=users,cn=accounts,dc=ald,dc=company,dc=lan       ivani   Иванов
   uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan    petrovss        Сидоров

Как мы видно на примере выше, выводится несколько колонок с разделителем табуляции ``\t``.

Конвертер ldif2json и работа с JSONPath
'''''''''''''''''''''''''''''''''''''''''''''''''''''

Для работы со структурированными данными можно также использовать формат **JSON**, и в Linux есть очень удобный процессор **JSON**, который называется **jq**. Вот пример Python-скрипта, который позволяет конвертировать поток **LDIF** в **JSON**, для его использования нужно создать файл, например *ldif2json.py*, и скопировать туда следующее содержимое:

.. code-block:: python

   #!/usr/bin/python3
   import sys
   import base64
   import json
   import re
   ### parse bool and int to json
   def parce_value(value):
     try:
       return int(value)
     except:
       if value == "FALSE" or value == "FALSE": return bool(value)
       else: return value
    
   if sys.version_info[0] < 3:
     raise Exception("Use Python 3: python3 ldif2json.py")
    
   data = sys.stdin.readlines()
   header_string = ""
   atrcheck = ""
   entry = ""
   dic_entries = {}
   dic_entry = {}
   current_dn = ""
   val_base64 = False
   ### main loop for parse headers and collect dict
   for line in data:
     ln = line.replace("\n", "").replace("\r", "")
     if len(ln) == 0:
       if not current_dn == "":
         dic_entries[current_dn] = dic_entry
         dic_entry = {}
         entry = ""
     else:
       if ln.startswith("version"):
         dic_entries["version"] = ln.split(": ")[1]
         continue
       elif ln.lstrip()[0] == "#":
         continue
    
       ### if line wrapped line starts with " " then add line to last attr
       elif ln[0] == " ":
         dic_entry[atrcheck] += ln.lstrip()
         if val_base64:
           val_to_decode = dic_entry[atrcheck]
           try:
             dic_entry[atrcheck] = base64.b64decode(val_to_decode).decode(
               'utf-8').strip()
           except:
             dic_entry[atrcheck] = val_to_decode
         continue
       entry += ln
       attribute = []
       attribute_name = ""
       attribute_value = ""
       if ln.find(":: ") > 0:
         val_base64 = True
         attribute = ln.split(":: ")
         try:
           attribute_value = base64.b64decode(
             attribute[1]).decode('utf-8').strip()
         except:
           attribute_value = attribute[1]
       elif ln.find(":< ") > 0:
         val_base64 = False
         attribute = ln.split(":< ")
         attribute_value = attribute[1]
       else:
         val_base64 = False
         attribute = ln.split(": ")
         try:
           attribute_value = re.sub(r"^.*?: ", "", ln)
         except:
           attribute_value = ""
       atrcheck = attribute[0].replace(":", "")
       ### get attribute and check if attribute exist
    
       if dic_entry.get(atrcheck):
         dic_entry[atrcheck] = str(
           dic_entry[atrcheck]) + "|" + str(attribute_value)
       else:
         dic_entry[atrcheck] = parce_value(attribute_value)
       if atrcheck == "dn":
         current_dn = attribute[1]
     if header_string.find(atrcheck) < 0:
       if header_string == "":
         header_string += atrcheck
       else:
         header_string += ";" + atrcheck
   ### add row if row not empty
   if entry != "":
     dic_entries[current_dn] = dic_entry
     dic_entry = {}
     entry = ""
    
   ### print stdout json from dict
   print(json.dumps(dic_entries, ensure_ascii=False))

Пример конвертации данных из **ldapsearch** через конвейер:

.. code-block:: bash

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(uid=*)" uid displayName sn mail rbtadb | python3 ldif2json.py

Результат выполнения:

.. code-block:: json

   {
     "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan": {
       "dn": "uid=admin,cn=users,cn=accounts,dc=ald,dc=company,dc=lan",
       "uid": "admin",
       "sn": "Administrator"
     },
     "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan": {
       "dn": "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan",
       "uid": "petrovss",
       "displayName": "Петр С.",
       "sn": "Сидоров",
       "mail": "petrov.ss@ald.company.lan"
     },
   }

Сохранить этот вывод в файл *users.json* можно простым перенаправлением и далее с помощью утилиты ``jq`` из одноименного пакета извлекать любые данные с помощью запросов **JSONPath**. Например, чтобы узнать значение атрибута ``mail`` для пользователя ``petrovss`` нужно ввести следующее:

.. code-block:: bash

   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(uid=*)" uid displayName sn mail rbtadb | python3 ldif2json.py > users.json && jq -r '."uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan".mail' users.json

где:

- параметр ``-r`` – означает, что выводить нужно сырые данные без кавычек;

- ``".”uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan.mail"`` - это текст запроса **JSONPath**.

Результат выполнения:

.. code-block:: bash

   petrov.ss@ald.company.lan

Для обработки полученных **JSON** данных в цикле, следует создать файл, например *json_cycle.sh*, со следующим содержимым:

.. code-block:: bash

   #!/bin/bash
   echo -e "uid\tdisplayName\tsn\tmail"
   ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(uid=*)" uid displayName sn mail rbtadb | python3 ldif2json.py > users.json
   cat users.json | jq -r 'delpaths([paths | select(length > 1)])' | jq -r '[paths | join(".")]'| jq -r 'join("\n")' | while read key
   do
    uid=$(jq -r ".\"$key\".uid" users.json)
    mail=$(jq -r ".\"$key\".mail" users.json)
    displayName=$(jq -r ".\"$key\".displayName" users.json)
    sn=$(jq -r ".\"$key\".sn" users.json)
    echo -e "$uid\t$displayName\t$sn\t$mail"
    ### можете добавить свои действия по обработке данных из файла
   done

Результат выполнения:

.. code-block:: console

   uid     displayName     sn      mail
   admin   null    Administrator   null
   petrovp Петров петр     Петров  petrovp@ald.company.lan
   ivani   Иван Иванов     Иванов  ivani@ald.company.lan
   petrovss        Петр С. Сидоров petrov.ss@ald.company.lan

Добавление пользователей
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

В этом примере при добавлении пользователя ``objectClass`` класс с именем ``ipantuserattrs`` не добавляется, чтобы атрибут ``ipaNTSecurityIdentifier`` сгенерировался автоматически. Содержимое файла *add_user.ldif*:

.. code-block:: bash

   dn: uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   objectClass: top
   objectClass: person
   objectClass: organizationalperson
   objectClass: inetorgperson
   objectClass: inetuser
   objectClass: posixaccount
   objectClass: krbprincipalaux
   objectClass: krbticketpolicyaux
   objectClass: ipaobject
   objectClass: ipasshuser
   objectClass: x-ald-user
   objectClass: x-ald-user-parsec14
   objectClass: x-ald-audit-policy
   objectClass: ruPostMailAccount
   objectClass: rbtaCustomUserAttrs
   objectClass: rbtaUserMeta
   objectClass: rbta-unit
   objectClass: rbta-address
   objectClass: rbta-inetorgperson-ext
   objectClass: ipaSshGroupOfPubKeys
   objectClass: mepOriginEntry
   givenName: Петр
   sn: Сидоров
   uid: petrovss
   cn: petrov.ss
   uidNumber: -1
   gidNumber: -1
   displayName: Петр С.
   initials: P.С.
   gecos: Тел. +71234567890
   rbtamiddlename: Сидоров
   rbtadp: ou=ald.company.lan,cn=orgunits,cn=accounts,dc=ald,dc=company,dc=lan
   loginShell: /bin/bash
   homeDirectory: /home/petrov.ss
   mail: petrov.ss@ald.company.lan
   x-ald-user-mac: 0:0x0:0:0x0
   krbCanonicalName: petrov.ss@ALD.COMPANY.LAN
   krbPrincipalName: petrov.ss@ALD.COMPANY.LAN
   userPassword: somepassword

.. note:: 

   Атрибуты ``uidNumber`` и ``gidNumber`` нужно устанавливать равными ``-1``, в этом случае DNA-плагин автоматически сгенерирует значения идентификаторов при добавлении пользователя. Пароль следует передавать открытым текстом. Он будет хэширован алгоритмом PBKDF2-SHA256 автоматически. Записать пользователю, сгенерированный где-то в другом месте hash-пароля возможно только при создании пользователя, если сервер переведен в режим миграции.

Далее выполняется команда по добавлению нового пользователя:

.. code-block:: bash

   ldapadd -Q < add_user.ldif

Результат выполнения:

.. code-block:: bash

   adding new entry "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

После входа пользователю нужно будет задать новый пароль, так как пароль назначенный таким способом является временным.

Теперь нужно добавить пользователей в цикле, используя данные из **CSV-файла** - создать файл, например *cycleadd.csv*, c разделителем ``;`` в любом тектовом редакторе:

.. code-block:: bash

   givenName;sn;uid
   Александр;Петров;alexp
   Михаил;Иванов;mikhaili
   Артём;Сидоров;artems

Затем создается скрипт ``add_from_csv.sh`` по добавлению пользователей из **CSV-файла**:

.. code-block:: bash

   #!/bin/bash
   csv_file=$1
   delim=$2
   function template()
   {
      psw=$(cat /dev/urandom| tr -dc '0-9a-zA-Z!@#$%^&*_+-' | head -c 14;echo;)
      uid=$1 #установить uid из первого параметра
      givenName=$2 #установить Имя из второго
      sn=$3 #установить Фамилию из третьего
      suffix=$(ldapsearch -Q -LLL -s base|grep 'dn:'|cut -d ' ' -f2) #получить suffix домена
      realm=$(hostname -d|tr '[a-zA-Z]' '[A-Za-z]')
      domain=$(hostname -d)
      cat <<EOF
   dn: uid=$uid,cn=users,cn=accounts,$suffix
   changetype: add
   objectClass: top
   objectClass: person
   objectClass: organizationalperson
   objectClass: inetorgperson
   objectClass: inetuser
   objectClass: posixaccount
   objectClass: krbprincipalaux
   objectClass: krbticketpolicyaux
   objectClass: ipaobject
   objectClass: ipasshuser
   objectClass: x-ald-user
   objectClass: x-ald-user-parsec14
   objectClass: x-ald-audit-policy
   objectClass: ruPostMailAccount
   objectClass: rbtaCustomUserAttrs
   objectClass: rbtaUserMeta
   objectClass: rbta-unit
   objectClass: rbta-address
   objectClass: rbta-inetorgperson-ext
   objectClass: ipaSshGroupOfPubKeys
   objectClass: mepOriginEntry
   givenName: $givenName
   sn: $sn
   uid: $uid
   cn: $uid
   uidNumber: -1
   gidNumber: -1
   displayName: $givenName ${sn:0:1}.
   initials: ${givenName:0:1}. ${sn:0:1}.
   rbtadp: ou=ald.company.lan,cn=orgunits,cn=accounts,$suffix
   loginShell: /bin/bash
   homeDirectory: /home/$uid
   mail: $uid@$domain
   x-ald-user-mac: 0:0x0:0:0x0
   krbCanonicalName: $uid@$realm
   krbPrincipalName: $uid@$realm
   userPassword: $psw


   EOF
   }
   echo Добавление пользователей из $csv_file
   truncate -s 0 cycleadd.ldif
   chmod 500 ./cycleadd.ldif
   while read line; do let c++;
      if [ $c -gt 1 ]; then
         p_givenName=$(echo ${line} | cut -d $delim -f 1 | sed "s/^\"//g" | sed "s/\"$//g" | sed "s/^\'//g" | sed "s/\'$//g")
         p_sn=$(echo ${line} | cut -d $delim -f 2 | sed "s/^\"//g" | sed "s/\"$//g"| sed "s/^\'//g" | sed "s/\'$//g")
         p_uid=$(echo ${line} | cut -d $delim -f 3 | sed "s/^\"//g" | sed "s/\"$//g" | sed "s/^\'//g" | sed "s/\'$//g")
         template $p_uid $p_givenName $p_sn >> cycleadd.ldif
      fi
   done < $csv_file
   ### добавим пользователей одним пакетом
   ldapadd -c -f cycleadd.ldif


Функцией ``template`` описан шаблон одной записи. Этот шаблон можно настроить по своему усмотрению, прописав свои корневые суффиксы и реалмы.

Далее следует назначить права запуска скрипту ``add_from_csv.sh`` и запустить его:

.. code-block:: bash

   chmod +x ./add_from_csv.sh && ./add_from_csv.sh cycleadd.csv ';'

Результат выполнения:

.. code-block:: bash

   Добавление пользователей из cycleadd.csv
   adding new entry "uid=alexp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"
   adding new entry "uid=mikhaili,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"
   adding new entry "uid=artems,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Скрипт ``add_from_csv.sh`` создал временный **LDIF-файл** *cycleadd.ldif* и передал команде **ldapadd**. После чего можно создать новый файл *new_users.csv* уже после добавления пользователей, потому что там уже сохранены новые пароли пользователей:

.. code-block:: bash

   cat cycleadd.ldif | python3 ldif2csv.py > new_users.csv

Файл *new_users.csv* можно скачать, для последующей рассылки пользователям паролей и учетных данных для входа, см :ref:`ldap-requests 18`.

.. figure:: media/рис18.png
   :name: ldap-requests 18

   Файл с новыми паролями для добавленных в цикле пользователей

Смена пароля пользователей
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

В случае взлома системы необходимо изменить пароли всех пользователей. Для этого создается скрипт на языке bash ``change_all_pass.sh``:

.. code-block:: bash

   #!/bin/sh
   read -p "Вы хотите создать новые пароли всем пользователям (Yes|no)? " yn
   if [[ $yn =~ "Yes" ]]; then
     echo '"dn";"password"' > new_passwords.csv
     echo "" > tmp_new_pass
     ldapsearch -Q -LLL -s one -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(!(uid=admin))" dn | while read line
     do
     if [ ! -z "${line}" ]; then
       psw=$(cat /dev/urandom| tr -dc '0-9a-zA-Z!@#$%^&*_+-' | head -c 14;echo;)
       echo "\"$line\";\"$psw\"" >> new_passwords.csv
       echo -e "${line}\nchangetype: modify\nreplace: userPassword\nuserPassword: $psw\n" >> tmp_new_pass
     fi
     done
     ldapmodify -Q < tmp_new_pass
   fi

Запуск скрипта:

.. code-block:: bash

   chmod +x change_all_pass.sh && ./change_all_pass.sh

Результат выполнения:

.. code-block:: bash

   Вы хотите создать новые пароли всем пользователям (Yes|no)? Yes
   modifying entry "uid=petrovp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"
   modifying entry "uid=ivani,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"
   modifying entry "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Далее следует подготовить **CSV-файл** *newpass.csv* с паролями из временного *tmp_new_pass* **LDIF** через конвертер ``ldif2CSV.py``:

.. code-block:: bash

   cat tmp_new_pass  | python3 ldif2csv.py > newpass.csv

Результат выполнения:

.. code-block:: bash

   "dn";"changetype";"replace";"userPassword"
   "uid=petrovp,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"modify";"userPassword";"l@q%yTU1hf_EP7"
   "uid=ivani,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"modify";"userPassword";"nM^ZER_z_sULpE"
   "uid=petrovss,cn=users,cn=accounts,dc=ald,dc=company,dc=lan";"modify";"userPassword";"oxK*HzeFmGJw5w"

А также можно открыть файл *newpass.csv* в редакторе электронных таблиц, см. :ref:`ldap-requests 19`.

.. figure:: media/рис19.png
   :name: ldap-requests 19

   Пакетное изменение паролей пользователей

Проверка просроченных паролей
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Проверка просроченных паролей пользователей с помощью скрипта ``check_expired.sh``:

.. code-block:: bash

   #!/bin/bash
   ### 1. Получить список пользователей из DN cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   ### 2. Отфильтровать список с полями uid, displayname, mail, krbPasswordExpiration
   ### 3. Оставить пользователей, у которых дата просрочки пароля (krbPasswordExpiration) от даты сегодня до даты сегодня + 7 дней
   ldapsearch -Q -LLL -o ldif-wrap=no -b "cn=users,cn=accounts,dc=ald,dc=company,dc=lan" "(&(krbPasswordExpiration>=$(date +"%Y%m%d000000Z"))(krbPasswordExpiration<=$(date +"%Y%m%d000000Z" -d "+7 days")))" uid displayname mail krbPasswordExpiration > expired_ldif
   while read line
   do ### 4. Цикл пользователь из пользователей
   if [ -z "${line}" ]; then
    if [ ! -z $user_mail ]; then
     ###   4.1 Если почта у пользователя есть
     timeExp=$(date -d "${date_expire:0:4}-${date_expire:4:2}-${date_expire:6:2} ${date_expire:8:2}:${date_expire:10:2}:${date_expire:12:2}Z" +"%s")
     timeNow=$(date +"%s")
     seconds=$(( timeExp - timeNow ))
     min=$(( seconds / 60 ))
     hours=$(( min / 60 ))
     days=$(( hours / 24 ))
     ago=$(echo "через $daysдн.")
     if [ $timeNow -gt $timeExp ]; then
      ago="просрочен!!"
     fi
     dateprint=$(date -d "@$timeExp")
     echo "$user_name ($user_mail), пароль просрочится ${dateprint} $ago"
     ### Тут вы можете добавить свою обработку пользователя.
   else
     ###   4.3 Иначе добавить пользователя в лог
     echo "$(date) [expire.sh] uid $user_uid mail is epmty" >> "expired.log"
    fi
    ### очистим переменные для следующей записи
    user_uid=""
    user_name=""
    user_mail=""
    date_expire=""
   else
     attr=$(echo $line | cut -d ":" -f 1)
     attvalue=$(echo $line | cut -d ":" -f 2)
     if [ -z "$attvalue" ]; then
       attvalue=$(echo $line | cut -d " " -f 2 | base64 -d)
     fi
     ### проверка атрибута и присвоение переменной значения
     case $attr in
       uid) user_uid=$(echo $attvalue | xargs echo -n);;
       displayname) user_name=$(echo $attvalue | xargs echo -n) ;;
       mail) user_mail=$(echo $attvalue | xargs echo -n);;
       krbPasswordExpiration) date_expire=$(echo $attvalue | xargs echo -n);;
     esac
   fi
   done < expired_ldif

Следует указать атрибут выполнения и запустить скрипт:

.. code-block:: bash

   chmod +x ./check_expired.sh && ./check_expired.sh

Результат выполнения:

.. code-block:: bash

   Петров петр (petrovp@ald.company.lan), пароль просрочится Вт мая 30 12:18:21 MSK 2023 через 2дн.
   Александр П. (alexp@ald.company.lan), пароль просрочится Сб мая 27 12:46:42 MSK 2023 просрочен!!
   Михаил И. (mikhaili@ald.company.lan), пароль просрочится Сб мая 27 12:46:42 MSK 2023 просрочен!!
   Артём С. (artems@ald.company.lan), пароль просрочится Сб мая 27 12:46:42 MSK 2023 просрочен!!

Скрипт обработал даты просрочки пароля, указав просроченные пароли и дни до их завершения. В данном примере использованы только bash и вывод **ldapsearch**. Также можно добавить в скрипт обработку просроченных учетных записей. Например, отправку сообщения в корпоративный мессенджер или на электронную почту.

Расширение схемы пользовательскими классами и атрибутами
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

При работе в домене **ALD Pro** появляется потребность в создании пользовательских атрибутов и классов для расширения схемы данных каталога, когда стандартные классы и атрибуты не позволяют описать в полной мере существующие объекты. Далее показано несколько способов реализации этой задачи. Для примеров использованы два контроллера домена: ``dc-1.ald.company.lan`` и  ``dc-2.ald.company.lan``.

Выбор OID
^^^^^^^^^

В каталоге классы и атрибуты имеют свой уникальный номер ``OID``. Чтобы получить ``OID``, необходимо обратиться к региональным или международным организациям, например, IANA, которые занимаются их регистрацией. На сайте https://oidref.com/org/marina-ignatyeva указаны контакты организации, которая ответственна за регистрацию ``OID``, начинающихся с ``1.2.643``. Базовый ``OID`` ``1.2.643`` специально выделен для России, а ``1.2.643.6`` для различных организаций внутри страны (https://oidref.com/1.2.643). Если ваша организация уже имеет свой собственный ``OID``, то лучше выбрать номера в соответствии с вашими внутренними правилами. Зарегистрированные номера предприятий можно посмотреть: https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers. При использовании ``OID``, которые еще не зарегистрированы, не рекомендуется публикация новой схемы данных вне своей организации. При назначении ``OID`` атрибутам и классам, следует быть внимательным, использование одного и того же номера может привести к проблемам в работе **LDAP-каталога**. Префиксы ``OID 1.3.6.1.4.1.32702`` и  ``1.3.6.1.4.1.52616`` являются зарезервированными и используется для служебных классов и атрибутов **ALD Pro** и **Astra Linux**. Префикс ``OID 2.16.840.1.113730.3.8`` используется для служебных классов и атрибутов **FreeIPA**. Их использование для расширения схемы может привести к сбоям в работе системы и невозможности ее корректного обновления.

В примерах выбран свободный незарегистрированный ``OID``, то есть в данный момент не принадлежащий какой-либо организации. ``OID`` состоит чисел, разделенных точками, число цифр не должно превышать ``128``.  Числа в ``OID`` могут иметь значения до ``2^64-1``. Cледует сгенерировать три больших числа, для того чтобы уменьшить вероятность их совпадения при выборе ``OID``:

.. code-block:: 

   dc-1:~$ shuf  -n 3 -i 1000000000-3000000000
   2685147094
   2741111200
   2726881669

Первое число соответствует номеру организации, поэтому базовый ``OID`` - ``1.2.643.6.2685147094``. В примере, внутри организации есть несколько подразделений и сервисов, за которые отвечают эти подразделения. Одному из подразделений присвоен номер ``2741111200`` и номер ``2726881669`` сервису или приложению, за которое ответственно это подразделение. В итоге ``OID`` для подразделения внутри организации будет ``1.2.643.6.2685147094.2741111200.2726881669``. Атрибутам сервиса соответствует число ``1``, а классам число ``2``. Поэтому ``OID`` для атрибутов начинается с ``1.2.643.6.2732288946.2741111200.2726881669.1``, а для классов с ``1.2.643.6.2732288946.2741111200.2726881669.2``.

В итоге ``OID`` - ``1.2.643.6.2685147094.2741111200.2726881669.x.y``:

* ``1.2.643.6`` – номер, выделенный для различных организаций в России;
* ``2732288946`` – номер организации;
* ``2741111200`` – номер подразделения внутри организации;
* ``2726881669`` – номер сервиса или приложения;
* ``x`` – тип объекта: атрибут или класс;
* ``y`` – номер атрибута или класса, каждый атрибут и класс должен иметь свой уникальный номер.

Возможны другие варианты выбора ``OID``, его составных частей, это зависит от принятой иерархии и правил внутри организации.

Создание классов и объектов
^^^^^^^^^^^^^^^^^^^^^^^^^^^

В примере требуется создать новый класс и новые атрибуты для хранения информации о рабочем месте пользователя. Рабочее место включает в себя номер кабинета, номер рабочего стола. Новый класс будет называться ``workplace`` и содержать два атрибута: название кабинета ``idCabinet`` и номер стола ``idTable``. Есть возможность не создавать новый класс, а атрибуты добавить в существующий класс, например, ``person``, но  так делать не рекомендутся, желательно не менять базовую схему каталога.

Примеры команд были запущены на стенде, состоящем из двух контроллеров домена ``dc-1.ald.company.lan`` и ``dc-2.ald.company.lan``. Команды в примерах могут быть запущены на любом компьютере домена, кроме утилиты **dsconf**, она доступна **только на контроллерах домена** и должна быть запущена под пользователем **root**.

Изменение схемы с помощью LDIF-файла
''''''''''''''''''''''''''''''''''''''''''''

Подготовка **LDIF-файла** для использования с утилитой ``ldapmodify`` и добавление в него определения класса и атрибутов:

.. code-block:: 

   dc-1:~$ cat workplace.ldif

   dn: cn=schema
   changetype: modify
   add: attributeTypes
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )
   -
   add: attributeTypes
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )
   -
   add: objectClasses
   objectClasses: ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MAY ( idCabinet $ idTable ) X-ORIGIN 'user defined' )

В файле:

* ``OID`` атрибута ``idCabinet`` равен ``1.2.643.6.2732288946.2741111200.2726881669.1.100``, в нем будет содержаться строковый тип данных, которому соответствует ``OID`` ``1.3.6.1.4.1.1466.115.121.1.15``. Например, “Приемная” или “Отдел статистики”.
* ``OID`` атрибута ``idTable`` равен ``1.2.643.6.2732288946.2741111200.2726881669.1.101``, он будет содержать числовой тип данных, которому соответствует ``OID`` ``1.3.6.1.4.1.1466.115.121.1.27``.
* ``OID`` класса ``workplace`` равен ``1.2.643.6.2732288946.2741111200.2726881669.2.50``, он имеет родительский класс ``top``. Класс содержит необязательные атрибуты ``idCabinet``, ``idTable``, если они должны быть обязательными, необходимо поменять ``MAY`` на ``MUST``.

Теперь следует добавить новые данные схемы в каталог:

.. code-block:: 

   dc-1:~$ ldapmodify -D "cn=Directory Manager" -W -f workplace.ldif
   Enter LDAP Password:
   
   modifying entry "cn=schema"

В ``cn=schema`` должны появиться новые данные:

.. code-block:: 

   dc-1:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema objectclasses query workplace
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   dc-1:~# 
   dc-1:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema attributetypes query idCabinet
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )

   MUST

   MAY
   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   dc-1:~#
   dc-1:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema attributetypes query idTable
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )

   MUST

   MAY
   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )

Текущая схема данных пока не обновлена, если была допущена какая-то неточность, надо удалить созданные классы и атрибуты, а затем снова создать их. Удаление будет рассмотрено в следующем разделе. Если все нормально и нет ошибок, для обновления схемы необходимо выполнить следующую команду и проверить результат:

.. code-block:: 

   dc-1:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema reload
   Enter password for cn=Directory Manager on ldap://localhost:

   Attempting to add task entry... This will fail if Schema Reload plug-in is not enabled.
   Successfully added task entry cn=schema_reload_2023-11-21T16:53:52.560422,cn=schema reload task,cn=tasks,cn=config
   To verify that the schema reload operation was successful, please check the error logs.

Если добавление прошло успешно, в лог файле */var/log/dirsrv/slapd-ALD-COMPANY-LAN/errors* должны быть следующие строки:

.. code-block:: 

   dc-1:~$ vi /var/log/dirsrv/slapd-ALD-COMPANY-LAN/errors
   ...
   [23/Nov/2023:07:21:37.108569846 +0300] - INFO - schemareload - schemareload_thread - Schema reload task starts (schema dir: default) ...
   [23/Nov/2023:07:21:37.249203959 +0300] - INFO - schemareload - schemareload_thread - Schema validation passed.
   [23/Nov/2023:07:21:37.370369733 +0300] - INFO - schemareload - schemareload_thread - Schema reload task finished.
   ...

Обновленная схема данных реплицируется на другие контроллеры домена не сразу, а только в том случае, если в каталоге произойдут какие-либо изменения, например, изменения какого-либо объекта каталога.

Создание нового класса к записи пользователя в каталоге для проверки, что схема данных реплицировалась на другой контроллер домена:

.. code-block:: 

   dc-1:~$ ldapmodify -D "cn=Directory Manager" -W
   Enter LDAP Password:

   dn: uid=user0,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   add: objectClass
   objectClass: workplace

   modifying entry "uid=user0,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Если теперь проверить на втором контроллере домена, то новый класс и новые атрибуты отобразятся в его каталоге:

.. code-block:: 

   dc-2:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema objectclasses query workplace
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   dc-2:~# 
   dc-2:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema attributetypes query idCabinet
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )

   MUST

   MAY
   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   dc-2:~#
   dc-2:~# dsconf -D "cn=Directory Manager" - W ldap://localhost schema attributetypes query idTable
   Enter password for cn=Directory Manager on ldap://localhost: 

   ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )

   MUST

   MAY
   ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MUST ( idCabinet $ idTable ) X-ORIGIN 'user defined' )

Удаление пользовательских классов и атрибутов
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Если после добавления классов и атрибутов изменения не были применены, то есть не была выполнена команда ``reload``, для удаления достаточно запустить команды ``dcsonf`` или ``ldapmodify``. Если же была применена команда ``reload`` и произошла репликация схемы на другие контроллеры, то перед удалением на каждом контроллере домена необходимо отключить репликацию схемы. Иначе новая схема снова появится на контроллере, на котором она была удалена, из-за репликации с другими контроллерами, так как они будут включать в себя более полный набор атрибутов и классов, включая новые данные. Когда один контроллер содержит схему с большим набором, чем второй, он реплицирует свой набор на первый. Сначала надо отключить репликацию схемы на каждом контроллере. В данном примере на двух.

На первом контроллере:

.. code-block::
   
   dc-1:~# dsconf -D "cn=Directory Manager" -W ALD-COMPANY-LAN config replace nsslapd-schemareplace=off
   Enter password for cn=Directory Manager on ALD-COMPANY-LAN:
   Successfully replaced "nsslapd-schemareplace"

На втором контроллере:

.. code-block::
   
   dc-2:~# dsconf -D "cn=Directory Manager" -W ALD-COMPANY-LAN config replace nsslapd-schemareplace=off
   Enter password for cn=Directory Manager on ALD-COMPANY-LAN:
   Successfully replaced "nsslapd-schemareplace"

После этого можно удалить новую схему с помощью утилиты ``dsconf`` или ``ldapmodify``, не опасаясь снова ее появления из-за репликации. Для удаления созданных атрибутов и классов команды запускаются на каждом контроллере:

.. code-block:: 

   dc-n:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema objectclasses remove workplace
   Enter password for cn=Directory Manager on ldap://localhost:

   Successfully removed the objectClass
   dc-n:~#
   dc-n:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema attributetypes remove idCabinet
   Enter password for cn=Directory Manager on ldap://localhost: 

   Successfully removed the attributetype
   dc-n:~$
   dc-n:~$ dsconf -D "cn=Directory Manager" -W ldap://localhost schema attributetypes remove idTable
   Enter password for cn=Directory Manager on ldap://localhost: 

   Successfully removed the attributetype

Или удаление с помощью **ldapmodify**:

.. code-block:: 

   dc-n:~# cat workplace_delete.ldif 

   dn: cn=schema
   changetype: modify
   delete: objectClasses
   objectClasses: ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MAY ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   -
   delete: attributeTypes
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )
   -
   delete: attributeTypes
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )
   dc-n:~#
   dc-n:~# ldapmodify -v -D "cn=Directory Manager" -W -f workplace_delete.ldif 
   ldap_initialize( <DEFAULT> )
   delete objectClasses:
   	( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MAY ( idCabinet $ idTable ) X-ORIGIN 'user defined' )
   delete attributeTypes:
   	( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )
   delete attributeTypes:
   	( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )
   modifying entry "cn=schema"
   modify complete

После удаления обязательно выполнить ``reload`` на каждом контроллере:

.. code-block:: 

   dc-n:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema reload
   Enter password for cn=Directory Manager on ldap://localhost:

   Attempting to add task entry... This will fail if Schema Reload plug-in is not enabled.
   Successfully added task entry cn=schema_reload_2023-11-21T16:53:52.560422,cn=schema reload task,cn=tasks,cn=config
   To verify that the schema reload operation was successful, please check the error logs.
   dc-n:~# 
   dc-n:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema objectclasses query workplace
   Enter password for cn=Directory Manager on ldap://localhost:
   None

Необходимо убедиться, что в лог-файле *error* нет ошибок:

.. code-block:: 

   dc-1:~# tail /var/log/dirsrv/slapd-ALD-COMPANY-LAN/errors
   ...
   [27/Nov/2023:07:04:59.065768985 +0300] - INFO - schemareload - schemareload_thread - Schema reload task starts (schema dir: default) ...
   [27/Nov/2023:07:04:59.192963349 +0300] - INFO - schemareload - schemareload_thread - Schema validation passed.
   [27/Nov/2023:07:04:59.314138040 +0300] - INFO - schemareload - schemareload_thread - Schema reload task finished.

После этого включить репликацию обратно на каждом контроллере:

.. code-block:: 

   dc-n:~# dsconf -D "cn=Directory Manager" -W ALD-COMPANY-LAN config replace nsslapd-schemareplace=replication-only
   Enter password for cn=Directory Manager on ALD-COMPANY-LAN:

   Successfully replaced "nsslapd-schemareplace"

Изменение схемы с помощью файла схемы
'''''''''''''''''''''''''''''''''''''''''''

Создание пользовательского класса и атрибутов в отдельном файле схемы и интеграция в директорию */etc/dirsrv/slapd-instance_name/schema/*. В данном примере это */etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/*. Имя файла и его содержимое должно удовлетворять нескольким критериям:

* в самом начале файла должна быть строка dn: ``cn=schema``;
* название файла должно быть в формате ``[1-9][0-9]text.ldif`` и всегда начинаться с двух цифр;
* важно, чтобы число в имени файла было меньше или равно числу в имени ``99user.ldif``;
* в случае, если число в имени равно ``99``, то в отсортированном алфавитном порядке ``text`` должен быть ниже, чем имя ``user`` в имени файла ``99user.ldif``. Например, имя ``99z.ldif`` будет неверным, а ``99a.ldif`` верным, так как буква “a” расположена в алфавите раньше буквы “u”;
* в файле атрибуты и классы могут быть определены одновременно или по отдельности, причем сначала должны идти описания атрибутов, а затем классов;
* в файле могут быть использованы уже существующие атрибуты, описанные в других классах.

**LDAP-сервер** будет читать файлы в директории */etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/* по порядку, начиная с файлов с именами, содержащими меньшие числа и заканчивая файлом с именем ``99user.ldif``. Если числа одинаковы, то в алфавитном порядке. Сервер ожидает, что самым последним прочитанным файлом будет *99user.ldif*. Когда добавляются новые элементы схемы, сервер записывает добавленные пользователем атрибуты и классы в файл, имя которого в директории */etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/* стоит последним после сортировки. Если имя последнего файла не совпадает с ``99user.ldif``, в работе каталога могут быть проблемы. Файл *99user.ldif** является служебным и всегда должен читаться сервером последним, напрямую в него не рекомендуется добавлять новую схему. Если в файлах встречаются определения одинаковых атрибутов и классов, определения загруженные позже перезаписывают предыдущие.

Нужно создать файл *99a.ldif* и добавить в него определения класса ``workplace`` и атрибутов ``idCabinet``, ``idTable``.

.. code-block:: 

   dc-1:~# cat /etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/99a.ldif

   dn: cn=schema
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCabinet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'user defined' )
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTable' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'user defined' )
   objectClasses: ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workplace' DESC 'Workplace for employee' SUP top AUXILIARY MAY ( idCabinet $ idTable ) X-ORIGIN 'user defined' )

Применение новой схемы:

.. code-block:: 

   dc-1:~# dsconf -D "cn=Directory Manager" -W ldap://localhost schema reload
   Enter password for cn=Directory Manager on ldap://localhost:

   Attempting to add task entry... This will fail if Schema Reload plug-in is not enabled.
   Successfully added task entry cn=schema_reload_2023-11-27T07:43:36.534129,cn=schema reload task,cn=tasks,cn=config
   To verify that the schema reload operation was successful, please check the error logs.

После репликации файл *99a.ldif* не будет скопирован на второй контроллер, а новый класс и новые атрибуты появятся на нем в файле */etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/99user.ldif*.

.. code-block:: 

   dc-2:~# grep -A 2 "workpl\|idCab\|idTab" /etc/dirsrv/slapd-ALD-COMPANY-LAN/schema/99user.ldif

   objectClasses: ( 1.2.643.6.2732288946.2741111200.2726881669.2.50 NAME 'workpla
    ce' DESC 'Workplace for employee' SUP top AUXILIARY MAY ( idCabinet $ idTable
     ) X-ORIGIN 'user defined' )
   attributeTypes: ( 2.16.840.1.113730.3.1.2384 NAME 'passwordTPRDelayValidFrom'
   --
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.100 NAME 'idCab
    inet' DESC 'Cabinet number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
     SINGLE-VALUE X-ORIGIN 'user defined' )
   --
   attributeTypes: ( 1.2.643.6.2732288946.2741111200.2726881669.1.101 NAME 'idTab
    le' DESC 'Table number for employee' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SIN
    GLE-VALUE X-ORIGIN 'user defined' )

Классы по умолчанию для пользователей и групп пользователей
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

В данный момент, при создании пользователей на Портале Управления **ALD Pro** записям пользователей в каталоге, находящимся в ``cn=users,cn=accounts,dc=ald,dc=company,dc=lan``, новый созданный класс не будет добавлен автоматически. Если создать пользователя ``user1`` и посмотреть список классов, он будет выглядеть так:

.. code-block:: 

   dc-1:~# ldapsearch -Q -LLL -D "cn=Directory Manager" -W -b "uid=user1,cn=users,cn=accounts,dc=ald,dc=company,dc=lan" objectclass
   Enter LDAP Password: 
   dn: uid=user0,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   objectclass: top
   objectclass: person
   objectclass: x-ald-user
   objectclass: x-ald-user-parsec14
   objectclass: x-ald-audit-policy
   objectclass: rbta-unit
   objectclass: rbta-address
   objectclass: rbtaCustomUserAttrs
   objectclass: rbta-inetorgperson-ext
   objectclass: ruPostMailAccount
   objectclass: rbtaUserMeta
   objectclass: ipaSshGroupOfPubKeys
   objectclass: mepOriginEntry
   objectclass: organizationalPerson
   objectclass: inetOrgPerson
   objectclass: inetUser
   objectclass: posixAccount
   objectclass: krbPrincipalAux
   objectclass: krbTicketPolicyAux
   objectclass: ipaObject
   objectclass: ipaSshUser
   objectclass: ipaNTUserAttrs

Команда ``ipa config-show`` покажет, какие классы по умолчанию добавляются к записи пользователя и к записи группы при их создании. Команда ``ipa`` должна быть запущена под администратором домена. Для этого надо войти под ним в систему или пройти **kerberos-аутентификацию** с помощью ``kinit``. Атрибут ``ipaUserObjectClasses`` в записи ``cn=ipaConfig,cn=etc,dc=ald,dc=company,dc=lan`` содержит все классы пользователя, а ``ipaGroupObjectClasses`` все классы для группы.

.. code-block:: 

   dc-1:~# ipa config-show --all --raw
     dn: cn=ipaConfig,cn=etc,dc=ald,dc=company,dc=lan
   ...
     cn: ipaConfig
     ipaGroupObjectClasses: top
     ipaGroupObjectClasses: groupofnames
     ipaGroupObjectClasses: nestedgroup
     ipaGroupObjectClasses: ipausergroup
     ipaGroupObjectClasses: ipaobject
     ipaGroupObjectClasses: x-ald-audit-policy
     ipaGroupObjectClasses: rbta-unit
     ipaUserObjectClasses: top
     ipaUserObjectClasses: person
     ipaUserObjectClasses: organizationalperson
     ipaUserObjectClasses: inetorgperson
     ipaUserObjectClasses: inetuser
     ipaUserObjectClasses: posixaccount
     ipaUserObjectClasses: krbprincipalaux
     ipaUserObjectClasses: krbticketpolicyaux
     ipaUserObjectClasses: ipaobject
     ipaUserObjectClasses: ipasshuser
     ipaUserObjectClasses: x-ald-user
     ipaUserObjectClasses: x-ald-user-parsec14
     ipaUserObjectClasses: x-ald-audit-policy
     ipaUserObjectClasses: rbta-unit
     ipaUserObjectClasses: rbta-address
     ipaUserObjectClasses: rbtaCustomUserAttrs
     ipaUserObjectClasses: rbta-inetorgperson-ext
     ipaUserObjectClasses: ruPostMailAccount
     ipaUserObjectClasses: rbtaUserMeta

С помощью ``ipa config-mod`` можно добавить новый класс ``workplace`` и тогда он будет добавляться автоматически к новым записям пользователей при их создании. В команде необходимо перечислить все классы через запятую, добавив в конце новый класс:

.. code-block:: 

   dc-1:~# ipa config-mod --userobjectclasses={top,person,organizationalperson,inetorgperson,inetuser,posixaccount,krbprincipalaux,krbticketpolicyaux,ipaobject,ipasshuser,x-ald-user,x-ald-user-parsec14,x-ald-audit-policy,rbta-unit,rbta-address,rbtaCustomUserAttrs,rbta-inetorgperson-ext,ruPostMailAccount,rbtaUserMeta,workplace}

   ...
     Классы объектов для пользователей по умолчанию: top, person, organizationalperson, inetorgperson, inetuser,
                                                     posixaccount, krbprincipalaux, krbticketpolicyaux,
                                                     ipaobject, ipasshuser, x-ald-user, x-ald-user-parsec14,
                                                     x-ald-audit-policy, rbta-unit, rbta-address,
                                                     rbtaCustomUserAttrs, rbta-inetorgperson-ext,
                                                     ruPostMailAccount, rbtaUserMeta, workplace
   ...

Аналогично для групп, если надо создать новый класс для групп, в команде вместо ключа ``--userobjectclasses`` используется ключ ``--groupobjectclasses``.

Удобнее добавлять новые классы через Портал Управления **ALD Pro** через пункты меню **Управление доменом** – **Пользователи и группы**. Справа будет список существующих классов, слева список классов по умолчанию. См. :ref:`ldap-requests 20`.

.. figure:: media/рис20.png
   :name: ldap-requests 20

   Параметры пользователей. Классы

На следующей вкладе доступны аналогичные настройки для групп. См. :ref:`ldap-requests 21`.

.. figure:: media/рис21.png
   :name: ldap-requests 21

   Параметры групп. Классы

Присвоение значений пользовательским атрибутам
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

После того, как класс добавлен для записей пользователей, атрибуты ``idCabinet``, ``idTable`` доступны для добавления и редактирования. Пользователь ``user2`` был создан через Портал Управления **ALD Pro**. Присвойте значения атрибутам ``idCabinet``, ``idTable`` для ``user2``:

.. code-block:: 

   dc-1:~# ldapmodify -D "cn=Directory Manager" -W
   Enter LDAP Password:

   dn: uid=user2,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   changetype: modify
   add: idCabinet
   idCabinet: Отдел статистики
   -
   add: idTable
   idTable: 10
   modifying entry "uid=user1,cn=users,cn=accounts,dc=ald,dc=company,dc=lan"

Команда завершена успешно, для проверки можно обратить внимание на атрибуты пользователя ``user2``:

.. code-block:: 

   dc-1:~# ldapsearch -Q -LLL -D "cn=Directory Manager" -W -b "uid=user2,cn=users,cn=accounts,dc=ald,dc=company,dc=lan" idCabinet idTable
   Enter LDAP Password: 

   dn: uid=user1,cn=users,cn=accounts,dc=ald,dc=company,dc=lan
   idCabinet:: 0J7RgtC00LXQuyDRgdGC0LDRgtC40YHRgtC40LrQuA==
   idTable: 10

Так как ``IdCabinet`` содержит кириллицу, в выводе команды он отображается в base64-кодировке.

Заключение
~~~~~~~~~~

**LDAP** разрабатывался с целью хранения любой информации об объектах на предприятии, таких как пользователи, компьютеры, серверы, подразделения и др. Информация удобно расположена в виде иерархического древа, которое строго типизировано объектными классами. База данных ориентирована на чтение, где быстро и легко можно получить любую информацию по объектам.

На сегодняшний день **LDAP-каталог** – это стандарт. Множество продуктов имеют встроенные интеграции с службой каталога, а также существует большое количество библиотек для разных языков для доступа к **LDAP-серверу**, например: python-ldap для python, ldaptive для java. В некоторые языки программирования уже встроена работа **LDAP**, например, в языки PHP и С#. А также у продукта **ALD Pro** есть REST API, через него можно управлять каталогом, используя простые Web-запросы.